Summing up students by the topics they studied and the marks they got
The main part of programming is writing code so that it makes sense to others, and the biggest part of that is slicing your code into functions (Single Responsibility) which do only one thing.
If I was changing your code, the first thing I would do is move everything under your entry point (if __name__... - good BTW, always have an entry point) into functions, and have the main communicate what the code is doing. For instance:

if __name__ == "__main__":
    load_student_files_into_data_frames()
    merge_test_marks()
    group_students()
    print_students_and_course_results()


Now I understand the purpose of your code without having to look for a README.txt or spending 5-10 minutes reading or stepping through your code. This is also a good technique for understanding program flow before you actually write code inside the functions (and lends itself to TDD, but that's another topic for another day). 

Of course, doing that with your code as it currently is, breaks all operations. 
Even if we update the load process to return each data frame, it starts to look messy:

def load_student_files_into_data_frames():
    students_df = pandas.read_csv("students.csv")
    marks_df = pandas.read_csv("marks.csv")
    tests_df = pandas.read_csv("tests.csv")
    courses_df = pandas.read_csv("courses.csv")
    return students_df, marks_df, tests_df, courses_df

if __name__ == "__main__":
    students_df, marks_df, tests_df, courses_df = load_student_files_into_data_frames()


Now we've encountered the DRY (Don't Repeat Yourself) code smell - performing the same operation multiple times. Let's extract out the CSV read into a separate function like this:

def read_csv(filename):
    return pandas.read_csv(filename)

def load_student_files_into_data_frames():
    file_names = ["students.csv", "marks.csv", "tests.csv", "courses.csv"]
    return map(read_csv, file_names)


However, now we violated the Open/Close Principle - which is that code should be closed for modification but open for enhancement. For example, if you need to add another CSV, you'd have to open the code up, edit it and put the new CSV in, correct? Or if the CSV comes from an external company, and they change its name (for whatever reason), you'd need to edit the code to make it work again, yes? This is true for magic numbers and anything else which can change.
So, the best method is to have the file names in an external .ini file, and use ConfigParser to load the names - but this is a small script, so we'll just define them in the __main__.

def load_student_files_into_data_frames(filename_list):
    return map(read_csv, filename_list)

if __name__ == "__main__":
    filename_list = ["students.csv", "marks.csv", "tests.csv", "courses.csv"]
    students_df, marks_df, tests_df, courses_df = load_student_files_into_data_frames(filename_list)


That's much cleaner, and now we're starting to write our functions in a functional programming style - which is we pass data in, get it modified, and get it passed out again. Nice and clean, and we know exactly what happens to the data where, making it very easy to narrow down bugs. 

For instance, the merge_test_marks() function becomes:

def merge_test_marks(marks_df, tests_df):
    return pandas.merge(marks_df, tests_df, left_on="test_id", right_on="id")


and if there were any issues with the merging of those two data frames, you know exactly which function was responsible for the bug (rather than having to laboriously step through the code with a debugger to understand where the variable's data state changed wrongly).

So here would be the final version, and whilst I haven't done anything to clean up the print_students_and_course_results function - as we have two internal loops that should be shuffled off to a separate function - I'll leave that as an exercise for you to attempt. Hope this was helpful, here is the final code (PEP8 formatted except I removed the extra line between functions to make it visually shorter):

import pandas
from statistics import mean

def read_csv(filename):
    return pandas.read_csv(filename)

def load_student_files_into_data_frames(filename_list):
    return map(read_csv, filename_list)

def merge_test_marks(marks_df, tests_df):
    return pandas.merge(marks_df, tests_df, left_on="test_id", right_on="id")

def group_students(marks_tests, students_df, courses_df):
    student_courses = marks_tests.groupby(by="student_id")['course_id'].apply(set)
    students_dict = students_df.set_index("id").to_dict(orient="index")
    courses_dict = courses_df.set_index("id").to_dict(orient="index")
    return student_courses, students_dict, courses_dict

def print_students_and_course_results(student_courses, students_dict, marks_tests, courses_dict):
    for student_id, course_ids in student_courses.iteritems():
        print("Student id: {}, name: {name}".format(student_id, **students_dict[student_id]))
        final_grades = []
        for course_id in course_ids:
            marks = marks_tests.loc[(marks_tests['course_id'] == course_id) &amp; (marks_tests['student_id'] == student_id)]
            final_grades.append(sum(marks['mark'] * (marks['weight'] / 100.0)))

        print("Total Average: {:.2f}%".format(mean(final_grades)))
        for course_id in course_ids:
            print("  Course: {name}, Teacher: {teacher}".format(**courses_dict[course_id]))
            marks = marks_tests.loc[(marks_tests['course_id'] == course_id) &amp; (marks_tests['student_id'] == student_id)]
            final_grade = sum(marks['mark'] * (marks['weight'] / 100.0))
            print("  Final Grade: {:.2f}%".format(final_grade))

if __name__ == "__main__":
    filename_list = ["students.csv", "marks.csv", "tests.csv", "courses.csv"]
    students_df, marks_df, tests_df, courses_df = load_student_files_into_data_frames(filename_list)
    marks_tests = merge_test_marks(marks_df, tests_df)
    student_courses, students_dict, courses_dict = group_students(marks_tests, students_df, courses_df)
    print_students_and_course_results(student_courses, students_dict, marks_tests, courses_dict)


Keep up the coding and Good Luck!